package pt.iscte.osgi.contracts.aspects;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import org.aspectj.lang.reflect.MethodSignature;

import pt.iscte.osgi.contracts.annotations.FirstInvocation;
import pt.iscte.osgi.contracts.annotations.MethodRequired;
import pt.iscte.osgi.contracts.annotations.NoInvocationByClients;
import pt.iscte.osgi.contracts.annotations.NotNull;
import pt.iscte.osgi.contracts.annotations.Require;
import pt.iscte.osgi.contracts.annotations.Requires;
import pt.iscte.osgi.contracts.annotations.SingleInvocation;


public abstract aspect MethodRequirements {
	
	private pointcut exclude() : !within(pt.iscte.osgi.contracts.aspects.*+);
	
	private HashMap<Object, Set<Method>> methodsUsed = new HashMap<Object, Set<Method>>();

	
	before(): execution(* *.*(..)) && exclude() {		
		Class<?> type = thisJoinPoint.getSignature().getDeclaringType();
		Method method = ((MethodSignature) thisJoinPoint.getSignature()).getMethod();
		Method[] methods = type.getDeclaredMethods();
		
		//nivel classe
		for (Annotation annotation : method.getAnnotations()) {
			if (annotation instanceof Requires) {
				for(Method m: methods){
					if (isInstanceOfRequiredMethod(m)) {
						verifyExceptionRequires(runMethod(m, thisJoinPoint.getThis()), m);
					}
				}
			} else {
				if (annotation instanceof Require) {
					for(Method m: methods){
						String value = ((Require) annotation).value();
						if (m.getName().equals(value)) {
							verifyExceptionRequires(runMethod(m, thisJoinPoint.getThis()), m);
						}
					}
				}
			}
		}
		
		//nivel interface
		String requiresMethodName = null, thisRequiresMethodName = null, thisRequireMethodName = null,
				requireMethodName = null;
		for (Class<?> interf : type.getInterfaces()) {
			for (Method m : interf.getDeclaredMethods()) {
				if (isInstanceOfRequiredMethod(m)) {
					requiresMethodName = m.getName();
				}
				if (isInstanceOfRequires(m)) {
					thisRequiresMethodName = m.getName();
				}
				String value = instanceOfRequire(m);
				if (value != null) {
					thisRequireMethodName = m.getName();
					requireMethodName = value;
				}
			}
		}
		
		if(method.getName().equals(thisRequiresMethodName)){
			for(Method m: methods){
				if(m.getName().equals(requiresMethodName)){
					verifyExceptionRequires(
							runMethod(m, thisJoinPoint.getThis()), m);
				}
			}		
		}
		
		if(method.getName().equals(thisRequireMethodName)){
			for(Method m: methods){
				if(m.getName().equals(requireMethodName)){
					verifyExceptionRequires(
							runMethod(m, thisJoinPoint.getThis()), m);
				}
			}		
		}
	}
	
	private boolean isInstanceOfRequires(Method m){
		for (Annotation ann : m.getAnnotations()) {
			if (ann instanceof Requires) {
				return true;
			}
		}
		return false;
	}
	
	
	private boolean isInstanceOfRequiredMethod(Method m){
		//m.isAnnotationPresent(MethodRequired.class)
		//MethodRequired ann = m.getAnnotation(MethodRequired.class);
		
		for (Annotation ann : m.getAnnotations()) {
			if (ann instanceof MethodRequired) {
				return true;
			}
		}
		return false;
	}
	
	
	private String instanceOfRequire(Method m){		
		for (Annotation ann : m.getAnnotations()) {
			if (ann instanceof Require) {
				return ((Require) ann).value();
			}
		}
		return null;
	}
		
	private void verifyExceptionRequires(Object value, Method method){
		if(value != null){
			if(!(Boolean)value)
//				System.out.println("method " + method.getName() + " has to return true when calling... ");
				throw new SecurityException("method " + method.getName() + " has to return true when calling... ");
		}
	}
	
	
	private Object runMethod(Method m, Object obj) {
		Object value = null;
		try {
			value = m.invoke(obj);
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
		return value;
	}
	
	
	after(): execution(*.new(..)) && exclude() { 
		Class<?> type = thisJoinPoint.getSignature().getDeclaringType();
		Method[] methods = type.getDeclaredMethods();
		
		//nivel classe
		for(Method m: methods){
			for(Annotation a: m.getAnnotations()){
				if(a instanceof FirstInvocation){
					runMethod(m, thisJoinPoint.getThis());
				}
			}
		}
		
		//nivel interface
		for (Class<?> interf : type.getInterfaces()) {
			String nome = null;
			for (Method m : interf.getDeclaredMethods()) {
				for (Annotation annotation : m.getAnnotations()) {
					if (annotation instanceof FirstInvocation) {
						nome = m.getName();
					}
				}
			}
			if (nome != null) {
				for (Method m : methods) {
					if(m.getName().equals(nome)){
						runMethod(m, thisJoinPoint.getThis());
					}
				}
			}
		}
		
	}
	
	before(): call(* *.*(..)) && exclude() {			
		Method method = ((MethodSignature) thisJoinPoint.getSignature()).getMethod();
		Class<?> type = thisJoinPoint.getSignature().getDeclaringType();
		
		//nivel classe
		validateAnnotations(method.getAnnotations(), thisJoinPoint.getTarget(), method);
		
		//nivel interface
		for (Class<?> interf : type.getInterfaces()) {			
			for (Method m : interf.getDeclaredMethods()) {
				if(m.getName().equals(method.getName())){					
					validateAnnotations(m.getAnnotations(), thisJoinPoint.getTarget(), method);
				}
			}
		}				
	}
	

	private void validateAnnotations(Annotation[] annotations, Object obj, Method method){
		for (Annotation annotation : annotations) {
			if (annotation instanceof NoInvocationByClients) {
//				System.out.println("method denied");
					throw new SecurityException("method denied");
			} else {
				if (annotation instanceof SingleInvocation) {							
					if(methodsUsed.containsKey(obj)){
						Set<Method> v = methodsUsed.get(obj);
						if(v.contains(method)){
//							System.out.println("can only access this method once");
								throw new SecurityException("can only access this method once");
						} else {
							v.add(method);
							methodsUsed.put(obj, v);
						}	
					} else {
						Set<Method> v = new HashSet<Method>();
						v.add(method);
						methodsUsed.put(obj, v);
					}	
				}
			}
		}
	}
	
	after() returning(Object obj): call(* *.get*(..)) && exclude() {
		Method method = ((MethodSignature) thisJoinPoint.getSignature()).getMethod();
		Class<?> type = thisJoinPoint.getSignature().getDeclaringType();
		
		//nivel classe
		validateReturnedObject(method.getAnnotations(), method, obj);
		
		//nivel interface
		for (Class<?> interf : type.getInterfaces()) {			
			for (Method m : interf.getDeclaredMethods()) {
				if(m.getName().equals(method.getName())){					
					validateReturnedObject(m.getAnnotations(), method, obj);
				}
			}
		}		
	}	
	
	private void validateReturnedObject(Annotation[] annotations, Method method, Object obj){
		for (Annotation annotation : annotations) {
			if (annotation instanceof NotNull) {							
				if(obj == null) {
//					System.out.println("Returning object cannot be null {" + method.getName() + "()}");
					throw new NullPointerException("Returning object cannot be null {" + method.getName() + "()}");
				}
			}
		}
	}
}
